package utils

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

type Download struct {
	errText      string
	targetFile   string
	savePath     string
	subPath      string
	filePath     string
	localPath    string
	md5Hash      string
	length       int64
	ext          string
	allowSize    int64
	allowExtList []string
	chunkSize    int64
	syncDo       *sync.Once
}

var downloadLocker sync.Mutex

func NewDownload(paths ...string) *Download {
	savePath := ""
	if len(paths) > 0 && paths[0] != "" {
		savePath = strings.Trim(paths[0], "/")
	} else {
		y, m, d := GetCurrentDate()
		savePath = fmt.Sprintf("%d/%d/%d/", y, m, d)
	}
	subPath := savePath
	if Configuration.Download.Path != "" {
		savePath = strings.TrimSuffix(Configuration.Download.Path, "/") + "/" + savePath
	}
	return &Download{
		savePath:     savePath,
		subPath:      subPath,
		filePath:     RandomString(32),
		allowSize:    Configuration.Download.MaxSize,
		allowExtList: Configuration.Download.AllowExt,
		chunkSize:    1024 * 1024,
	}
}

func (d *Download) SetSavePath(path string) *Download {
	d.savePath = path
	return d
}

func (d *Download) SetFilename(filePath string) *Download {
	d.filePath = filePath
	return d
}

func (d *Download) SetMaxSize(size int64) *Download {
	d.allowSize = size
	return d
}

func (d *Download) SetAllowExt(extList []string) *Download {
	d.allowExtList = extList
	return d
}

func (d *Download) SetChunkSize(size int64) *Download {
	d.chunkSize = size
	return d
}

// GetError get upload error
func (d *Download) GetError() string {
	return d.errText
}

func (d *Download) GetSubPath() string {
	return d.subPath
}

// GetFullPath get success url list
func (d *Download) GetFullPath() string {
	return d.localPath
}

func (d *Download) GetRelativePath() string {
	return d.localPath
}

func (d *Download) GetHash(hashType string) (string, error) {
	if hashType == "md5" && d.md5Hash != "" {
		return d.md5Hash, nil
	}
	f, err := os.Open(d.localPath)
	if err != nil {
		return "", err
	}
	d.md5Hash, err = GetFileHash(f, hashType)
	if err != nil {
		return "", err
	}
	return d.md5Hash, nil
}

func (d *Download) GetUniqueFilename(hashType ...string) string {
	if len(hashType) == 0 {
		hashType = make([]string, 0)
		hashType = append(hashType, "md5")
	}
	hash, err := d.GetHash(hashType[0])
	if err != nil {
		return fmt.Sprintf("%s.%s", d.filePath, d.ext)
	}
	return fmt.Sprintf("%s.%s", hash, d.ext)
}

// GetFileSize 获取文件头信息
func (d *Download) GetFileSize(url string) bool {
	resp, err := http.Head(url)
	if err != nil {
		d.errText = fmt.Sprintf("获取文件头信息失败，%s", err.Error())
		return false
	}
	d.targetFile = url
	defer resp.Body.Close()
	if !d.CheckResponse(resp) {
		return false
	}
	return true
}

// CheckResponse 验证文件
func (d *Download) CheckResponse(resp *http.Response) bool {
	if d.allowSize > 0 && d.allowSize < resp.ContentLength {
		d.errText = fmt.Sprintf("超出文件最大%dM限制", d.allowSize/1024/1024)
		return false
	}

	contentType := resp.Header.Get("Content-Type")
	es := strings.Split(contentType, "/")
	if len(es) != 2 || es[1] == "" {
		d.errText = fmt.Sprintf("获取文件类型失败，%s", contentType)
		return false
	}

	if len(d.allowExtList) > 0 && !InMap(es[1], d.allowExtList) {
		d.errText = fmt.Sprintf("文件类型%s不允许操作", es[1])
		return false
	}
	d.ext = es[1]
	d.length = resp.ContentLength
	random := RandomString(10)
	d.filePath = Ternary(d.filePath != "", d.filePath, random)
	rootPath, _ := os.Getwd()
	d.localPath = filepath.Join(rootPath, d.savePath, d.filePath)
	d.localPath += "." + d.ext
	return true
}

// Save 单线程下载文件
func (d *Download) Save(file string) bool {
	_, err := os.Stat(d.savePath)
	if os.IsNotExist(err) {
		err = os.MkdirAll(d.savePath, 0755)
		if err != nil {
			d.errText = fmt.Sprintf("创建本地缓存目录失败：%s", err.Error())
			return false
		}
	}

	if !d.GetFileSize(file) {
		return false
	}

	// 创建 http 请求头
	req, err := http.NewRequest("GET", file, nil)
	if err != nil {
		d.errText = fmt.Sprintf("获取文件失败, %s", err.Error())
		return false
	}
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", 0, d.length))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		d.errText = fmt.Sprintf("获取文件失败-2, %s", err.Error())
		return false
	}
	defer resp.Body.Close()

	downloadLocker.Lock()
	defer downloadLocker.Unlock()

	if !d.CheckResponse(resp) {
		return false
	}

	f, err := os.OpenFile(d.localPath, os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		_, err = os.Stat(d.localPath)
		if err == nil {
			return true
		}
		d.errText = fmt.Sprintf("创建文件失败-1, %s, path: %s", err.Error(), d.localPath)
		return false
	}
	defer f.Close()

	// 将 http 响应体写进硬盘的文件
	buf := make([]byte, 16*1024)
	_, err = io.CopyBuffer(f, resp.Body, buf)
	if err != nil {
		if err == io.EOF {
			d.errText = fmt.Sprintf("读取文件超时, %s", err.Error())
			return false
		}
		d.errText = fmt.Sprintf("保存文件失败, %s", err.Error())
		return false
	}
	return true
}

// SaveBySlice 多线程切片下载文件
func (d *Download) SaveBySlice(file string) bool {
	_, err := os.Stat(d.savePath)
	if os.IsNotExist(err) {
		err = os.MkdirAll(d.savePath, 0755)
		if err != nil {
			d.errText = fmt.Sprintf("创建本地缓存目录失败：%s", err.Error())
			return false
		}
	}
	taskChan := make(chan [2]int64, runtime.NumCPU())
	wg := new(sync.WaitGroup)

	if !d.GetFileSize(file) {
		return false
	}
	// 创建执行下载任务的 worker
	err = d.initWorker(taskChan, wg)
	if err != nil {
		d.errText = fmt.Sprintf("初始化切片下载任务失败, %s", err.Error())
		return false
	}

	// 分发下载任务
	if !d.dispatchTask(taskChan) {
		return false
	}

	// 等待所有下载任务完成
	wg.Wait()

	return true
}

// 初始化 下载 worker
func (d *Download) initWorker(taskChan chan [2]int64, wg *sync.WaitGroup) error {
	for i := 0; i < runtime.NumCPU(); i++ {
		// 打开文件句柄
		file, err := os.OpenFile(d.localPath, os.O_CREATE|os.O_RDWR, 0644)
		if err != nil {
			return err
		}

		wg.Add(1)
		go func(file *os.File, taskChan chan [2]int64) {
			defer wg.Done()
			defer file.Close()

			// 循环从 taskCh 中获取下载任务并下载
			for part := range taskChan {
				log.Printf("downloading part, start offset: %d, end offset: %d", part[0], part[1])

				// 重试下载，最大重试次数为 10 次，每次下载失败后等待 1 秒
				err := d.retryWithWaitTime(10, func() error {
					return d.downloadPart(file, part[0], part[1])
				}, time.Second)
				if err != nil {
					log.Printf("download part %d failed: %v", part, err)
				}
			}
		}(file, taskChan)
	}

	return nil
}

// 分发下载任务
func (d *Download) dispatchTask(taskCh chan [2]int64) bool {
	defer close(taskCh)
	parts := d.length / d.chunkSize

	log.Println("file size:", d.length, "parts:", parts, "chunk size:", d.chunkSize)

	for i := int64(0); i < parts; i++ {
		// 计算分片的起始和结束位置
		startOffset := i * d.chunkSize
		endOffset := startOffset + d.chunkSize - 1

		// 发送下载任务
		taskCh <- [2]int64{startOffset, endOffset}
	}

	// 发送最后一个分片的下载任务
	if d.length%d.chunkSize != 0 {
		taskCh <- [2]int64{parts * d.chunkSize, d.length - 1}
	}
	return true
}

// 下载文件分片
func (d *Download) downloadPart(file *os.File, startPos, endPos int64) error {
	req, err := http.NewRequest("GET", d.targetFile, nil)
	if err != nil {
		return errors.New(fmt.Sprintf("文件连接失败, %s", err.Error()))
	}
	// 设置文件分片区间的请求头
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", startPos, endPos))
	resp, err := http.DefaultTransport.RoundTrip(req)
	if err != nil {
		return errors.New(fmt.Sprintf("同步文件失败, %s", err.Error()))
	}
	defer resp.Body.Close()

	// 如果服务器返回的状态码不是 206 Partial Content，则说明下载失败
	if resp.StatusCode != http.StatusPartialContent {
		data, err := io.ReadAll(resp.Body)
		if err != nil {
			return errors.New(fmt.Sprintf("同步文件失败, %s", err.Error()))
		}
		log.Println("unexpected data:", string(data))
		return errors.New(fmt.Sprintf("unexpected status code: %d", resp.StatusCode))
	}

	// 文件指针移动到分片的起始位置
	_, err = file.Seek(startPos, 0)
	if err != nil {
		return errors.New(fmt.Sprintf("同步文件失败-2, %s", err.Error()))
	}

	// 写入分片数据到文件
	_, err = io.Copy(file, resp.Body)
	if err != nil {
		return errors.New(fmt.Sprintf("同步文件失败-3, %s", err.Error()))
	}

	return nil
}

// 重试函数
func (d *Download) retryWithWaitTime(retryCount int, fn func() error, waitTime time.Duration) error {
	var err error
	for i := 0; i < retryCount; i++ {
		e := fn()
		if e != nil {
			errors.Join(err, e)
			time.Sleep(waitTime)
			continue
		}

		return nil
	}

	return err
}
